/*
 * Copyright (c) 2006-2018, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author           Notes
 * 2019-08-09     heyuanjie87      first version
 */

#include <rtthread.h>
#include <drivers/usb_host.h>
#include <drivers/usb/cdc.h>

#ifdef USBH_CDCACM_ENABLE_DBG
#define DBG_ENABLE
#endif
#define DBG_LVL DBG_LOG 
#define DBG_TAG "cdcacm"
#include <rtdbg.h>

#if 1//def RT_USBH_CDCACM
#include <dfs_posix.h>
#include <dfs_poll.h>

struct uh_cdcacm
{
    struct rt_device parent;
    rt_list_t files;
    rt_list_t r_comp;

    struct upipe *pipe_in;
    struct upipe *pipe_out;

    rt_wqueue_t rxwq;
    rt_wqueue_t txwq;

    struct rt_mutex wlock;
    struct rt_mutex rlock;
    struct rt_mutex lock;

    uint16_t hungup;
    uint16_t minor;
};

static int read_submit(struct uh_cdcacm *acm)
{
    struct uhintf *intf;
    struct urb *u;

    intf = (struct uhintf*)acm->parent.user_data;

    u = rt_usbh_urb_idle_take(acm->pipe_in);
    if (!u)
        return -EBUSY;

    u->size = u->bufsize;

    return rt_usbh_urb_submit(intf->device, acm->pipe_in, u);
}

static int _cdcacm_readbuf(struct uh_cdcacm *acm, uint8_t *buf, size_t size)
{
    int len = 0;
    struct urb *u;

    if (rt_list_isempty(&acm->r_comp))
        return 0;
    
    u = rt_list_first_entry(&acm->r_comp, struct urb, link);
    if (u->actual_size)
    {
        if (size > u->actual_size)
            len = u->actual_size;
        else
            len = size;

        rt_memcpy(buf, &u->buf[u->pos], len);
        u->pos += len;
        u->actual_size -= len;
    }

    if ((u->actual_size == 0) && (u->pos != 0))
    {
        rt_list_remove(&u->link);
        rt_list_insert_before(&acm->pipe_in->urbidle, &u->link);

        read_submit(acm);
    }

    return len;
}

static int _raw_read(struct uh_cdcacm *acm, int nb, uint8_t *buf, size_t size)
{
    int ret = 0;
    int c;

    while (1)
    {
        if (acm->hungup)
            break;

        c = _cdcacm_readbuf(acm, buf, size);
        if (c == 0)
        {
            if (nb && (ret == 0))
            {
                ret = -EAGAIN;
                break;
            }
            if (ret)
                break;

            rt_wqueue_wait(&acm->rxwq, acm->hungup, -1);
        }
        else
        {
            size -= c;
            buf += c;
            ret += c;
        }
    }

    return ret;
}

static int _cdcacm_writebuf(struct uh_cdcacm *acm, const uint8_t *buf, size_t size)
{
    int ret;
    struct uhintf *intf = (struct uhintf*)acm->parent.user_data;
    int wsize;
    struct urb *u;

    u = rt_usbh_urb_idle_take(acm->pipe_out);
    if (!u)
        return 0;

    if (size > u->bufsize)
        wsize = u->bufsize;
    else
        wsize = size;

    rt_memcpy(u->buf, buf, wsize);
    u->size = wsize;
    ret = rt_usbh_urb_submit(intf->device, acm->pipe_out, u);

    if (ret == 0)
        ret = wsize;
    else
        ret = 0;

    return ret;
}

static int _raw_write(struct uh_cdcacm *acm, int nb, const uint8_t *buf, size_t size)
{
    int ret = -EAGAIN;
    int c;
    const uint8_t *dbuf;

    dbuf = buf;
    while (1)
    {
        if (acm->hungup)
            break;

        while (size > 0)
        {
            c = _cdcacm_writebuf(acm, dbuf, size);
            if (!c)
                break;

            dbuf += c;
            size -= c;
        }

        if (!size)
            break;
        if (nb)
            break;

        rt_wqueue_wait(&acm->txwq, acm->hungup, -1);
    }

    return (dbuf == buf) ? ret : (dbuf - buf);
}

static int cdcacm_set_line_code(struct uh_cdcacm *acm, struct ucdc_line_coding *lc)
{
    int ret;
    struct urequest setup;
    struct uhintf *intf = (struct uhintf*)acm->parent.user_data;

    setup.request_type = USB_REQ_TYPE_DIR_OUT | USB_REQ_TYPE_CLASS | USB_REQ_TYPE_INTERFACE;
    setup.bRequest     = CDC_SET_LINE_CODING;
    setup.wValue       = 0;
    setup.wIndex       = intf->intf_desc->bInterfaceNumber;
    setup.wLength      = sizeof(struct ucdc_line_coding);

    ret = rt_usbh_control_msg(intf->device, &setup, lc, sizeof(*lc), USB_TIMEOUT_LONG);

    return ret;
}

static int _cdcacm_active(struct uh_cdcacm *acm)
{
    struct ucdc_line_coding lc;

    lc.bCharFormat = 0;
    lc.bDataBits = 8;
    lc.bParityType = 0;
    lc.dwDTERate = 115200;

    return cdcacm_set_line_code(acm, &lc);
}

static int _cdcacm_wlock(struct uh_cdcacm *acm, int nb)
{
    if (nb && rt_mutex_take(&acm->wlock, 0))
        return -EAGAIN;

    rt_mutex_take(&acm->wlock, -1);

    return 0;
}

static void _cdcacm_wunlock(struct uh_cdcacm *acm)
{
    rt_mutex_release(&acm->wlock);
}

static int _cdcacm_rlock(struct uh_cdcacm *acm, int nb)
{
    if (nb && rt_mutex_take(&acm->rlock, 0))
        return -EAGAIN;

    rt_mutex_take(&acm->rlock, -1);

    return 0;
}

static void _cdcacm_runlock(struct uh_cdcacm *acm)
{
    rt_mutex_release(&acm->rlock);
}

static void _cdcacm_lock(struct uh_cdcacm *acm)
{
    rt_mutex_take(&acm->lock, -1);
}

static void _cdcacm_unlock(struct uh_cdcacm *acm)
{
    rt_mutex_release(&acm->lock);
}

static void _cdcacm_kill_urb_all(struct uh_cdcacm *acm)
{
    struct uhintf *intf;
    struct urb *u;

    intf = (struct uhintf*)acm->parent.user_data;

    rt_usbh_urb_killall(intf->device, acm->pipe_in);
    rt_usbh_urb_killall(intf->device, acm->pipe_out);
}

static int _cdcacm_open(struct uh_cdcacm *acm)
{
    if (acm->parent.ref_count != 0)
        return 0;

    //_cdcacm_active(acm);
    return read_submit(acm);
}

static void _cdcacm_close(struct uh_cdcacm *acm)
{
    _cdcacm_kill_urb_all(acm);
}

/* fops for cdcacm */
static int cdcacm_fops_open(struct dfs_fd *fd)
{
    int ret = -ENODEV;
    struct uh_cdcacm *acm;

    acm = (struct uh_cdcacm *)fd->data;

    rt_list_init(&fd->dev);

    _cdcacm_lock(acm);
    if (acm->hungup)
        goto _out;

    ret = _cdcacm_open(acm);
    if (ret == 0)
    {
        acm->parent.ref_count ++;
        rt_list_insert_after(&acm->files, &fd->dev);
    }

_out:
    _cdcacm_unlock(acm);

    return ret;
}

static int cdcacm_fops_close(struct dfs_fd *fd)
{
    int ret = 0;
    struct uh_cdcacm *acm;

    acm = (struct uh_cdcacm *)fd->data;
    if (acm->hungup)
        return 0;

    _cdcacm_lock(acm);
    if (acm->parent.ref_count > 0)
    {
        acm->parent.ref_count --;
        if (acm->parent.ref_count == 0)
        {
            _cdcacm_close(acm);
        }
    }
    rt_list_remove(&fd->dev);
    _cdcacm_unlock(acm);

    return ret;
}

static int cdcacm_fops_ioctl(struct dfs_fd *fd, int cmd, void *args)
{
    int ret;

    return ret;
}

static int cdcacm_fops_read(struct dfs_fd *file, void *buf, size_t size)
{
    int ret;
    struct uh_cdcacm *acm;
    int nb;

    if (size == 0)
        return 0;

    acm = file->data;
    nb = file->flags & O_NONBLOCK;

    ret = _cdcacm_rlock(acm, nb);
    if (ret)
        return ret;

    ret = _raw_read(acm, nb, buf, size);

    _cdcacm_runlock(acm);

    return ret;
}

static int cdcacm_fops_write(struct dfs_fd *file, const void *buf, size_t size)
{
    int ret;
    struct uh_cdcacm *acm;
    int nb;

    if (size == 0)
        return 0;

    acm = file->data;
    nb = file->flags & O_NONBLOCK;

    ret = _cdcacm_wlock(acm, nb);
    if (ret)
        return ret;

    ret = _raw_write(acm, nb, buf, size);

    _cdcacm_wunlock(acm);

    return ret;
}

static int cdcacm_fops_poll(struct dfs_fd *fd, struct rt_pollreq *req)
{
    int mask = 0;
    struct uh_cdcacm *acm;

    acm = (struct uh_cdcacm *)fd->data;
    _cdcacm_lock(acm);
    if (acm->hungup)
        goto _out;

_out:
    _cdcacm_unlock(acm);

    return mask;
}

const static struct dfs_file_ops _cdcacm_fops =
{
    cdcacm_fops_open,
    cdcacm_fops_close,
    cdcacm_fops_ioctl,
    cdcacm_fops_read,
    cdcacm_fops_write,
    RT_NULL, /* flush */
    RT_NULL, /* lseek */
    RT_NULL, /* getdents */
    cdcacm_fops_poll,
};

static struct uclass_driver cdcacm_driver;

static const struct udevice_match cdcacm_match[] =
{
    {USB_INTERFACE_MATCH(0xff, 0, 0)},
    {0}
};

static void cdcacm_write_callback(struct urb *r)
{
    struct uh_cdcacm *acm;

    acm = (struct uh_cdcacm *)r->context;
    rt_list_insert_before(&acm->pipe_out->urbidle, &r->link);

    rt_wqueue_wakeup(&acm->txwq, (void *)POLLOUT);
}

static void cdcacm_read_callback(struct urb *r)
{
    struct uh_cdcacm *acm;

    acm = (struct uh_cdcacm *)r->context;

    if (r->status == 0)
        rt_list_insert_before(&acm->r_comp, &r->link);
    else
        rt_list_insert_before(&acm->pipe_in->urbidle, &r->link);

    rt_wqueue_wakeup(&acm->rxwq, (void *)POLLIN);
}

static int cdcacm_urb_init(struct uh_cdcacm *acm)
{
    int ret = -1;

    rt_usbh_urb_allocall(acm->pipe_in, 64, cdcacm_read_callback, acm, 1);
    rt_usbh_urb_allocall(acm->pipe_out, 64, cdcacm_write_callback, acm, 1);

    ret = 0;

    return ret;
}

static int cdcacm_init(struct uh_cdcacm *acm)
{
    if (cdcacm_urb_init(acm) != 0)
    {
        return -ENOMEM;
    }

    rt_wqueue_init(&acm->txwq);
    rt_wqueue_init(&acm->rxwq);
    rt_list_init(&acm->files);
    rt_list_init(&acm->r_comp);
    rt_mutex_init(&acm->wlock, "acmw", 0);
    rt_mutex_init(&acm->rlock, "acmr", 0);
    rt_mutex_init(&acm->lock, "acme", 0);

    return 0;
}

static int cdcacm_getminor(void)
{
    int idx;
    int ret = -1;

    for (idx = 0; idx < 10; idx ++)
    {
        if (!(cdcacm_driver.minors & (1<<idx)))
        {
            cdcacm_driver.minors |= (1<<idx);
            ret = idx;
            break;
        }
    }

    return ret;
}

static void cdcacm_putminor(int m)
{
    cdcacm_driver.minors &= (~(1<<m));
}

static int cdcacm_register(struct uh_cdcacm *acm)
{
    int ret;
    char name[8];

    acm->minor = cdcacm_getminor();
    if (acm->minor < 0)
        return -1;
    rt_sprintf(name, "ttyACM%d", acm->minor);

    return rt_device_register(&acm->parent, name, 0);
}

static void cdcacm_hungup(struct uh_cdcacm *acm)
{
    struct dfs_fd *fd, *tmp;

    acm->hungup = 1;

    _cdcacm_lock(acm);
    rt_list_for_each_entry_safe(fd, tmp, struct dfs_fd, &acm->files, dev)
    {
        dfs_file_hungup(fd);
    }
    _cdcacm_unlock(acm);

    /* writers&readers will be wakeup */
    _cdcacm_kill_urb_all(acm);

    /* wait writers&readers exit */
    _cdcacm_wlock(acm, 0);
    _cdcacm_wunlock(acm);
    _cdcacm_rlock(acm, 0);
    _cdcacm_runlock(acm);
}

static void _cdcacm_cleanup(void *arg)
{
    struct uh_cdcacm *acm = arg;

    LOG_D("cleanup");

    rt_mutex_detach(&acm->rlock);
    rt_mutex_detach(&acm->wlock);
    rt_mutex_detach(&acm->lock);

    rt_free(acm);
}

static rt_err_t cdcacm_enable(void *arg)
{
    int ret;
    int interface;
    struct uhintf *intf = (struct uhintf*)arg;
    int nep;
    struct uh_cdcacm *acm;

    acm = rt_calloc(1, sizeof(*acm));
    if (!acm)
        return -ENOMEM;

    for (nep = 0; nep < intf->intf_desc->bNumEndpoints; nep ++)
    {
        uep_desc_t ep_desc;

        /* get endpoint descriptor */        
        rt_usbh_get_endpoint_descriptor(intf->intf_desc, nep, &ep_desc);
        if(ep_desc == RT_NULL) 
        {
            LOG_E("get_endpoint_descriptor error\r\n");
            return -1;
        }
        if(USB_EP_ATTR(ep_desc->bmAttributes) != USB_EP_ATTR_BULK)
            continue;
        if (ep_desc->bEndpointAddress & USB_DIR_IN)
            ret = rt_usb_hcd_alloc_pipe(intf->device->hcd, &acm->pipe_in, intf->device, ep_desc); 
        else
            ret = rt_usb_hcd_alloc_pipe(intf->device->hcd, &acm->pipe_out, intf->device, ep_desc); 

        if(ret != 0)
        {
            LOG_W("alloc pipe fail");
            goto _out;
        }
    }

    if ((ret = cdcacm_init(acm)) != 0)
        goto _out;

    ret = cdcacm_register(acm);
    if (ret == 0)
    {
        intf->user_data = acm;
        acm->parent.user_data = intf;
        acm->parent.fops = &_cdcacm_fops;
    }
    else
    {
        //todo free urbs
    }

    LOG_D("intf register(%d)", ret);

_out:
    if (ret != 0)
        rt_free(acm);

    return ret;
}

static rt_err_t cdcacm_disable(void *arg)
{
    struct uhintf *intf = (struct uhintf*)arg;
    struct uh_cdcacm *acm;

    acm = (struct uh_cdcacm *)intf->user_data;

    cdcacm_hungup(acm);
    rt_device_unregister(&acm->parent);
    cdcacm_putminor(acm->minor);
    rt_usbh_cleanup_submit(_cdcacm_cleanup, acm);

    return 0;
}

int rt_usbh_class_cdcacm_init(void)
{
    cdcacm_driver.match = cdcacm_match;//USB_CLASS_CDC;

    cdcacm_driver.enable = cdcacm_enable;
    cdcacm_driver.disable = cdcacm_disable;

    rt_usbh_class_driver_register(&cdcacm_driver);

    return 0;
}
INIT_COMPONENT_EXPORT(rt_usbh_class_cdcacm_init);
#endif
